package client;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import security.KeyLoader;
import security.MasterChannel;
import security.exception.ConnectFailedException;
import security.exception.HandShakeFailedException;
import security.exception.KeyLoadingFailedException;
import server.data.User;

/**
 * Auction-Client, used to connect to an Auction-Server. Users can create
 * Auctions, list Auctions and bid on Auctions. The Client sends Messages via
 * TCP and receives Answers. The Client is also listening for UDP-Messages from
 * the server.
 *
 * @author Gruppe 95
 */
public class Client implements Stoppable {

    private static final Logger logger = Logger.getLogger("Client");
    private static final String COMM_CLIENT_LIST_HIDDEN = "!getClientList-hidden";
    private static final String COMM_GET_TIMESTAMP = "!getTimestamp";
    private static final String COMM_BID = "!bid";
    private static final String COMM_SIGNED_BID = "!signedBid";
    private static final int MASTERCHANNEL_ENCRYPTED = 1;
    private Timer timer;
    private final String serverHost;
    private final int tcpPort;
    private final int udpPort;
    private MasterChannel masterChannel;
    private BufferedReader brStdIn;
    private Thread serverOutageThread;
    private Socket timestampRequestSocket;
    private PrintWriter timestampRequestOut;
    private BufferedReader timestampRequestIn;
    private boolean bStop = false;
    private boolean blocked = false;
    private boolean lostConnection;
    private ArrayList<User> users;
    private ArrayList<String> signedBids;
    private PublicKey publicKey = null;
    private PrivateKey privateKey = null;
    private Thread tcpInputThread;

    /**
     * Client Constructor
     *
     * @param serverHost Host name or IP of the auction server
     * @param tcpPort TCP connection port on which the auction server is
     * listening for connections
     * @param udpPort will be used for UDP-Notifications from the auction server
     * @throws NumberFormatException if tcpPort or udpPort aren't numeric
     */
    public Client(final String serverHost, final String tcpPort, final String udpPort) throws NumberFormatException, KeyLoadingFailedException {
        this.serverHost = serverHost;
        this.tcpPort = Integer.parseInt(tcpPort);
        this.udpPort = Integer.parseInt(udpPort);
        users = new ArrayList<User>();
        signedBids = new ArrayList<String>();
        lostConnection = false;
        publicKey = KeyLoader.loadPublicKey("auction-server");
    }

    /**
     * Starts the client, creates a Thread for incoming TCP-Messages and listens
     * for commands on the command line. Therefore Sockets are created, ....
     */
    public void start() {
        try {
            synchronized (this) {
                masterChannel = new MasterChannel(serverHost, tcpPort);
                logger.log(Level.INFO, "create masterchannel {0} ", new Object[]{masterChannel});
                tcpInputThread = new Thread(new TCPListenerThread(masterChannel, this));
            }
            tcpInputThread.start();

            brStdIn = new BufferedReader(new InputStreamReader(System.in));
            String strLine;
            while (!bStop) {
                try {
                    if (brStdIn.ready()) {
                        strLine = brStdIn.readLine();
                        if ("!end".equals(strLine)) {
                            this.stop();
                        } else if (!blocked) {
                            if (masterChannel.getUser() == null) {
                                if (strLine.startsWith("!login")) {
                                    logger.info("login-command client");
                                    doLogin(strLine);
                                } else if (strLine.equals("!list")) {
                                    masterChannel.sendMessageHMAC(strLine);
                                } else {
                                    System.out.println("Invalid command.");
                                }
                            } else {
                                if (strLine.equals("!logout")) {
                                    logger.info("logout client - set back channel");
                                    if (timer != null) {
                                        timer.cancel();
                                    }
                                    privateKey = null;
                                    masterChannel.logoutClient();
                                } else if (strLine.startsWith("!confirm")) {
                                    masterChannel.sendMessage(strLine);
                                    setBlocked(true);
                                } else if (strLine.equals("!list")) {
                                    masterChannel.sendMessageHMAC(strLine);
                                }
                                else if (strLine.startsWith(COMM_BID)) {
                                    try {
                                        masterChannel.sendMessage(strLine);
                                    } catch (IOException ioex) {
                                        System.out.println("call for timestamps");
                                        askTimestamps(strLine);
                                        throw ioex;
                                    }
                                } else {
                                    logger.log(Level.INFO, "send message through masterchannel {0} ", new Object[]{strLine});
                                    masterChannel.sendMessage(strLine);
                                }

                            }
                        } else {

                            System.out.println("Blocked - waiting for group-bid to be confirmed.");
                        }

                    }
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        //ignore
                    }
                } catch (IOException ioex) {
                    System.out.println("IOException occured " + ioex.getMessage());
                    this.connectionLost();
                }
            }
        } catch (UnknownHostException uhe) {
            System.out.println("The Server '" + serverHost + "' could not be found!");
        } catch (IOException ioex) {
            System.out.println("IOException occured " + ioex.getMessage());
            this.connectionLost();
        }
    }

    private void doLogin(final String strLine) {
        final String[] parts = strLine.split(" ");
        if (parts.length > 1) {
            String username = parts[1];
            try {
                privateKey = KeyLoader.loadPrivateKey(username);
                logger.log(Level.INFO, "login user {0} private key {1} public key {2}", new Object[]{username, privateKey, publicKey});

                masterChannel.doClientHandShake(username, udpPort, privateKey, publicKey);
                if (masterChannel.getState() == MASTERCHANNEL_ENCRYPTED) {
                    timer=new Timer();
                    timer.scheduleAtFixedRate(new UserlistTimer(), 1000, 5000);
                    serverOutageThread = new Thread(new ServerOutageThread(masterChannel.getUser(), privateKey));
                    serverOutageThread.start();
                }
            } catch (KeyLoadingFailedException ex) {
                System.out.println("Login failed, couldn't load '" + username + "'s key!");
                logger.info("login failed in client " + ex.getMessage());
            } catch (HandShakeFailedException ex) {
                System.out.println(ex.getMessage());
                logger.info("handshake failed in client " + ex.getMessage());
            } catch (IOException ex) { //from ServerOutageThread
                logger.log(Level.SEVERE, null, ex);
            }
        }
    }

    @Override
    public synchronized void stop() {
        if (!bStop) {
            bStop = true;
            try {
                if (brStdIn != null) {
                    brStdIn.close();
                }
            } catch (IOException e) {
                //ignore
            }
            if (masterChannel != null) {
                masterChannel.close();
            }
            //@todo serveroutagethread stoppen
            if(masterChannel.getUser()!=null)
            {
                this.timer.cancel();
            }
            System.out.println("Client stopped!");
        }
    }

    /**
     * Sets the boolean to block a user for 20 seconds after a "!confirm"
     * comman.
     *
     * @param blocked
     */
    public void setBlocked(boolean blocked) {
        this.blocked = blocked;
    }

    /**
     * Updates the client's list of currently online users.
     *
     * @param input
     */
    public void updateUsers(String input) {
        users.clear();
        String[] values;
        String[] address;
        User tempUser;
        String[] clients = input.split(System.getProperty("line.separator"));
        for (String c : clients) {
            values = c.split(" ");
            address = values[0].split(":");
            tempUser = new User(values[2]);
            try {
                tempUser.setIAClientAddress(InetAddress.getByName(address[0]));
            } catch (UnknownHostException ex) {
                Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
            }
            //@todo: punkt aus der portaddresse herausnehmen
            tempUser.setiPClientPort(Integer.valueOf(address[1]));
            users.add(tempUser);
            //logger.log(Level.INFO, "add user: {0} {1} {2}", new Object[]{tempUser.toString(), tempUser.getIAClientAddress().toString(), tempUser.getiPClientPort()});
        }
    }

    /**
     * This Method is called when a lost connection to the server is detected.
     * Then it does something.....
     */
    private synchronized void connectionLost() {
        if (!lostConnection) {
            System.out.println("Lost Server-Connection detected");
            lostConnection = true;
//            tcpInputThread; stirbt sowieso
            if(timer!=null)
            {
                timer.cancel();
            }
            timer=new Timer();
            timer.scheduleAtFixedRate(new ReconnectorTask(), 1000, 5000);
        }
    }

    /**
     * This method is called when trying to bid after server-outage. Asks 2
     * other online users to validate the bid-request with timestamp and
     * signature. Signed bid is saved in "signedBids" to send when server is
     * back on.
     *
     * @param bid The original bid-request.
     */
    private synchronized void askTimestamps(String bid) {
        int requestCount = 0;
        String message;
        String[] timestamps = new String[2];
        if (users.size() > 2) {
            message = COMM_GET_TIMESTAMP + bid.substring(COMM_BID.length());
            logger.log(Level.INFO, "ask timestamp more than 2 users: {0}", new Object[]{message});
            while (requestCount < 2) {
                for (User u : users) {
                    if (!u.toString().equalsIgnoreCase(masterChannel.getUser().toString())) {
                        if ((timestamps[requestCount] = sendTimestampRequest(u, message)) != null) {
                            logger.log(Level.INFO, "new signedBid: {0}", new Object[]{timestamps[requestCount]});
                            requestCount++;
                        }
                    }
                }
            }
            signedBids.add(COMM_SIGNED_BID + " " + message + " " + timestamps[0] + " " + timestamps[1]);

        } else {
            System.out.println("Not enough users online to send timestamp-requests.");
        }
    }

    /**
     * Sends a timestamp-request to another user. Establish a socketConnection
     * with other user, send timestamprequest, receive timestamp. Returns null
     * if no timestamp is requested.
     *
     * @param u
     * @param message
     * @return
     */
    private synchronized String sendTimestampRequest(User u, String message) {
        String received = null;
        String out = null;
        try {
            System.out.println(u.getIAClientAddress().toString() + " port: " + u.getiPClientPort());
            timestampRequestSocket = new Socket(u.getIAClientAddress(), u.getiPClientPort());
            logger.log(Level.INFO, "timestampRequestSocket {0} ", new Object[]{timestampRequestSocket.getInetAddress().toString()});
            timestampRequestOut = new PrintWriter(timestampRequestSocket.getOutputStream(), true);
            timestampRequestIn = new BufferedReader(new InputStreamReader(timestampRequestSocket.getInputStream()));
        } catch (IOException ex) {
            logger.log(Level.INFO, "timestampRequestSocket connection failed: {0} ", new Object[]{ex.getMessage()});
        }
        timestampRequestOut.println(message);
        try {
            if ((received = timestampRequestIn.readLine()) != null) {
                String parts[] = received.split(" ");
                out = u.toString() + ":" + parts[3] + ":" + parts[4];
                logger.log(Level.INFO, "received timestamp: {0} ", new Object[]{out});

            }
            timestampRequestIn.close();
            timestampRequestOut.close();
            timestampRequestSocket.close();
        } catch (IOException ex) {
            logger.log(Level.INFO, "timestampRequestSocket readLine failed");
        }
        return out;
    }

    private class UserlistTimer extends TimerTask {

        @Override
        public void run() {
            try {
                masterChannel.sendMessage(COMM_CLIENT_LIST_HIDDEN);
            } catch (IOException ex) {
                logger.log(Level.SEVERE, null, ex);
                connectionLost();
            }
        }
    }

    private class ReconnectorTask extends TimerTask {

        @Override
        public void run() {
            try {
                masterChannel.reconnect();
                //if not reconnected successfully the following cannot be reached
                try {
                    timer.cancel();
                    lostConnection = false;
                    tcpInputThread = new Thread(new TCPListenerThread(masterChannel, Client.this));
                    tcpInputThread.start();

                    if(masterChannel.getUser()!=null)
                    {
                        masterChannel.doClientHandShake(masterChannel.getUser().getStrName(), tcpPort, privateKey, publicKey);
                        if(signedBids!=null)
                        {
                            try
                            {
                                while(!signedBids.isEmpty())
                                {
                                    masterChannel.sendMessage(signedBids.get(0));
                                    signedBids.remove(0);
                                }
                            }catch(IOException ex)
                            {
                                logger.log(Level.SEVERE, null, ex);
                                connectionLost();
                            }
                        }
                        
                        timer=new Timer();
                        timer.scheduleAtFixedRate(new UserlistTimer(), 3000, 5000);
                    }
                } catch (HandShakeFailedException ex) {
                    logger.log(Level.SEVERE, "Reconnecting to Server failed", ex);
                }

            } catch (ConnectFailedException ex) {
                logger.log(Level.SEVERE, null, ex);
            }
        }
    }
}
